Getting Started

Magma is a hardware construction language written in Python 3. The central abstraction in Magma is a Circuit, which is analagous to a verilog module. A circuit is a set of functional units that are wired together.

Magma is designed to work with Mantle, a library of hardware building blocks including logic and arithmetic units, registers, memories, etc.

The Loam system builds upon the Magma Circuit abstraction to represent parts and boards. A board consists of a set of parts that are wired together. Loam makes it is easy to setup a board such as the Lattice IceStick.

Lattice IceStick

In this tutorial, we will be using the Lattice IceStick. This breakout board contains a ICE40HX FPGA with 1K 4-input LUTs. The board has several useful peripherals including an FTDI USB interface with an integrated JTAG interface which is used to program the FPGA and a USART which is used to communicate with the host. The board also contains 5 LEDs, a PMOD interface, and 2 10-pin headers (J1 and J3). The 10-pin headers bring out 8 GPIO pins, as well as power and ground. This board is inexpensive ($25), can be plugged into the USB port on your laptop, and, best of all, can be programmed using an open source software toolchain.

Additional information about the IceStick Board can be found in the IceStick Programmers Guide

As a first example, let's write a Magma program that blinks an LED on the Icestick Board.

First, we import Magma as the module m. Next, we import Counter from Mantle. Before doing the import we configure mantle to use the ICE40 as the target device.


In [1]:
import magma as m
m.set_mantle_target("ice40")

The next step is to setup the IceStick board. We import the class IceStick from Loam. We then create an instance of an IceStick. This board instance has member variables that store the configuration of all the parts on the board. The blink program will use the Clock and the LED D5. Turning on the Clock and the LED D5 sets up the build environment to use the associated ICE40 GPIO pins.


In [2]:
from loam.boards.icestick import IceStick

# Create an instance of an IceStick board
icestick = IceStick()

# Turn on the Clock 
# The clock must turned on because we are using a synchronous counter
icestick.Clock.on()

# Turn on the LED D5
icestick.D5.on();


import lattice ice40
import lattice mantle40

Now that the IceStick setup is done, we create a main program that runs on the Lattice ICE40 FPGA. This main program becomes the top level module.

We create a simple circuit inside main. The circuit has a a 22-bit counter wired to D5. The crystal connected to the ICE40 has a frequency of 12 Mhz. so the counter will increment at that rate. Wiring the most-significant bit of the counter to D5 will cause the LED to blink roughly 3 times per second. D5 is accessible via main. In a similar way, the output of the counter is accesible via counter.O, and since this an array of bits we can access the MSB using Python's standard list indexing syntax.


In [3]:
from mantle import Counter

N = 22

# Define the main Magma Circuit on the FPGA on the IceStick
main = icestick.DefineMain()

# Instance a 22-bit counter
counter = Counter(N)

# Wire bit 21 of the counter's output to D5.
main.D5 <= counter.O[N-1]

# End main
m.EndDefine()

We then compile the program to verilog. This step also creates a PCF (physical constraints file).


In [4]:
m.compile('build/blink', main)

Now we run the open source tools for the Lattice ICE40. yosys synthesizes the input verilog file (blink.v) to produce an output netlist (blink.blif). arachne-pnr runs the place and router and generates the bitstream as a text file. icepack creates a binary bitstream file that can be downloaded to the FPGA. iceprog uploads the bitstream to the device. Once the device has been programmed, you should see the center, green LED blinking.


In [5]:
%%bash
cd build
yosys -q -p 'synth_ice40 -top main -blif blink.blif' blink.v
arachne-pnr -q -d 1k -o blink.txt -p blink.pcf blink.blif 
icepack blink.txt blink.bin
#iceprog blink.bin


/Users/hanrahan/git/magmathon/notebooks/tutorial/icestick/build

You can view the verilog file generated by Magma.


In [6]:
%cat build/blink.v


module FullAdder (input  I0, input  I1, input  CIN, output  O, output  COUT);
wire  SB_LUT4_inst0_O;
wire  SB_CARRY_inst0_CO;
SB_LUT4 #(.LUT_INIT(16'h9696)) SB_LUT4_inst0 (.I0(I0), .I1(I1), .I2(CIN), .I3(1'b0), .O(SB_LUT4_inst0_O));
SB_CARRY SB_CARRY_inst0 (.I0(I0), .I1(I1), .CI(CIN), .CO(SB_CARRY_inst0_CO));
assign O = SB_LUT4_inst0_O;
assign COUT = SB_CARRY_inst0_CO;
endmodule

module Add22_COUT (input [21:0] I0, input [21:0] I1, output [21:0] O, output  COUT);
wire  FullAdder_inst0_O;
wire  FullAdder_inst0_COUT;
wire  FullAdder_inst1_O;
wire  FullAdder_inst1_COUT;
wire  FullAdder_inst2_O;
wire  FullAdder_inst2_COUT;
wire  FullAdder_inst3_O;
wire  FullAdder_inst3_COUT;
wire  FullAdder_inst4_O;
wire  FullAdder_inst4_COUT;
wire  FullAdder_inst5_O;
wire  FullAdder_inst5_COUT;
wire  FullAdder_inst6_O;
wire  FullAdder_inst6_COUT;
wire  FullAdder_inst7_O;
wire  FullAdder_inst7_COUT;
wire  FullAdder_inst8_O;
wire  FullAdder_inst8_COUT;
wire  FullAdder_inst9_O;
wire  FullAdder_inst9_COUT;
wire  FullAdder_inst10_O;
wire  FullAdder_inst10_COUT;
wire  FullAdder_inst11_O;
wire  FullAdder_inst11_COUT;
wire  FullAdder_inst12_O;
wire  FullAdder_inst12_COUT;
wire  FullAdder_inst13_O;
wire  FullAdder_inst13_COUT;
wire  FullAdder_inst14_O;
wire  FullAdder_inst14_COUT;
wire  FullAdder_inst15_O;
wire  FullAdder_inst15_COUT;
wire  FullAdder_inst16_O;
wire  FullAdder_inst16_COUT;
wire  FullAdder_inst17_O;
wire  FullAdder_inst17_COUT;
wire  FullAdder_inst18_O;
wire  FullAdder_inst18_COUT;
wire  FullAdder_inst19_O;
wire  FullAdder_inst19_COUT;
wire  FullAdder_inst20_O;
wire  FullAdder_inst20_COUT;
wire  FullAdder_inst21_O;
wire  FullAdder_inst21_COUT;
FullAdder FullAdder_inst0 (.I0(I0[0]), .I1(I1[0]), .CIN(1'b0), .O(FullAdder_inst0_O), .COUT(FullAdder_inst0_COUT));
FullAdder FullAdder_inst1 (.I0(I0[1]), .I1(I1[1]), .CIN(FullAdder_inst0_COUT), .O(FullAdder_inst1_O), .COUT(FullAdder_inst1_COUT));
FullAdder FullAdder_inst2 (.I0(I0[2]), .I1(I1[2]), .CIN(FullAdder_inst1_COUT), .O(FullAdder_inst2_O), .COUT(FullAdder_inst2_COUT));
FullAdder FullAdder_inst3 (.I0(I0[3]), .I1(I1[3]), .CIN(FullAdder_inst2_COUT), .O(FullAdder_inst3_O), .COUT(FullAdder_inst3_COUT));
FullAdder FullAdder_inst4 (.I0(I0[4]), .I1(I1[4]), .CIN(FullAdder_inst3_COUT), .O(FullAdder_inst4_O), .COUT(FullAdder_inst4_COUT));
FullAdder FullAdder_inst5 (.I0(I0[5]), .I1(I1[5]), .CIN(FullAdder_inst4_COUT), .O(FullAdder_inst5_O), .COUT(FullAdder_inst5_COUT));
FullAdder FullAdder_inst6 (.I0(I0[6]), .I1(I1[6]), .CIN(FullAdder_inst5_COUT), .O(FullAdder_inst6_O), .COUT(FullAdder_inst6_COUT));
FullAdder FullAdder_inst7 (.I0(I0[7]), .I1(I1[7]), .CIN(FullAdder_inst6_COUT), .O(FullAdder_inst7_O), .COUT(FullAdder_inst7_COUT));
FullAdder FullAdder_inst8 (.I0(I0[8]), .I1(I1[8]), .CIN(FullAdder_inst7_COUT), .O(FullAdder_inst8_O), .COUT(FullAdder_inst8_COUT));
FullAdder FullAdder_inst9 (.I0(I0[9]), .I1(I1[9]), .CIN(FullAdder_inst8_COUT), .O(FullAdder_inst9_O), .COUT(FullAdder_inst9_COUT));
FullAdder FullAdder_inst10 (.I0(I0[10]), .I1(I1[10]), .CIN(FullAdder_inst9_COUT), .O(FullAdder_inst10_O), .COUT(FullAdder_inst10_COUT));
FullAdder FullAdder_inst11 (.I0(I0[11]), .I1(I1[11]), .CIN(FullAdder_inst10_COUT), .O(FullAdder_inst11_O), .COUT(FullAdder_inst11_COUT));
FullAdder FullAdder_inst12 (.I0(I0[12]), .I1(I1[12]), .CIN(FullAdder_inst11_COUT), .O(FullAdder_inst12_O), .COUT(FullAdder_inst12_COUT));
FullAdder FullAdder_inst13 (.I0(I0[13]), .I1(I1[13]), .CIN(FullAdder_inst12_COUT), .O(FullAdder_inst13_O), .COUT(FullAdder_inst13_COUT));
FullAdder FullAdder_inst14 (.I0(I0[14]), .I1(I1[14]), .CIN(FullAdder_inst13_COUT), .O(FullAdder_inst14_O), .COUT(FullAdder_inst14_COUT));
FullAdder FullAdder_inst15 (.I0(I0[15]), .I1(I1[15]), .CIN(FullAdder_inst14_COUT), .O(FullAdder_inst15_O), .COUT(FullAdder_inst15_COUT));
FullAdder FullAdder_inst16 (.I0(I0[16]), .I1(I1[16]), .CIN(FullAdder_inst15_COUT), .O(FullAdder_inst16_O), .COUT(FullAdder_inst16_COUT));
FullAdder FullAdder_inst17 (.I0(I0[17]), .I1(I1[17]), .CIN(FullAdder_inst16_COUT), .O(FullAdder_inst17_O), .COUT(FullAdder_inst17_COUT));
FullAdder FullAdder_inst18 (.I0(I0[18]), .I1(I1[18]), .CIN(FullAdder_inst17_COUT), .O(FullAdder_inst18_O), .COUT(FullAdder_inst18_COUT));
FullAdder FullAdder_inst19 (.I0(I0[19]), .I1(I1[19]), .CIN(FullAdder_inst18_COUT), .O(FullAdder_inst19_O), .COUT(FullAdder_inst19_COUT));
FullAdder FullAdder_inst20 (.I0(I0[20]), .I1(I1[20]), .CIN(FullAdder_inst19_COUT), .O(FullAdder_inst20_O), .COUT(FullAdder_inst20_COUT));
FullAdder FullAdder_inst21 (.I0(I0[21]), .I1(I1[21]), .CIN(FullAdder_inst20_COUT), .O(FullAdder_inst21_O), .COUT(FullAdder_inst21_COUT));
assign O = {FullAdder_inst21_O,FullAdder_inst20_O,FullAdder_inst19_O,FullAdder_inst18_O,FullAdder_inst17_O,FullAdder_inst16_O,FullAdder_inst15_O,FullAdder_inst14_O,FullAdder_inst13_O,FullAdder_inst12_O,FullAdder_inst11_O,FullAdder_inst10_O,FullAdder_inst9_O,FullAdder_inst8_O,FullAdder_inst7_O,FullAdder_inst6_O,FullAdder_inst5_O,FullAdder_inst4_O,FullAdder_inst3_O,FullAdder_inst2_O,FullAdder_inst1_O,FullAdder_inst0_O};
assign COUT = FullAdder_inst21_COUT;
endmodule

module Register22 (input [21:0] I, output [21:0] O, input  CLK);
wire  SB_DFF_inst0_Q;
wire  SB_DFF_inst1_Q;
wire  SB_DFF_inst2_Q;
wire  SB_DFF_inst3_Q;
wire  SB_DFF_inst4_Q;
wire  SB_DFF_inst5_Q;
wire  SB_DFF_inst6_Q;
wire  SB_DFF_inst7_Q;
wire  SB_DFF_inst8_Q;
wire  SB_DFF_inst9_Q;
wire  SB_DFF_inst10_Q;
wire  SB_DFF_inst11_Q;
wire  SB_DFF_inst12_Q;
wire  SB_DFF_inst13_Q;
wire  SB_DFF_inst14_Q;
wire  SB_DFF_inst15_Q;
wire  SB_DFF_inst16_Q;
wire  SB_DFF_inst17_Q;
wire  SB_DFF_inst18_Q;
wire  SB_DFF_inst19_Q;
wire  SB_DFF_inst20_Q;
wire  SB_DFF_inst21_Q;
SB_DFF SB_DFF_inst0 (.C(CLK), .D(I[0]), .Q(SB_DFF_inst0_Q));
SB_DFF SB_DFF_inst1 (.C(CLK), .D(I[1]), .Q(SB_DFF_inst1_Q));
SB_DFF SB_DFF_inst2 (.C(CLK), .D(I[2]), .Q(SB_DFF_inst2_Q));
SB_DFF SB_DFF_inst3 (.C(CLK), .D(I[3]), .Q(SB_DFF_inst3_Q));
SB_DFF SB_DFF_inst4 (.C(CLK), .D(I[4]), .Q(SB_DFF_inst4_Q));
SB_DFF SB_DFF_inst5 (.C(CLK), .D(I[5]), .Q(SB_DFF_inst5_Q));
SB_DFF SB_DFF_inst6 (.C(CLK), .D(I[6]), .Q(SB_DFF_inst6_Q));
SB_DFF SB_DFF_inst7 (.C(CLK), .D(I[7]), .Q(SB_DFF_inst7_Q));
SB_DFF SB_DFF_inst8 (.C(CLK), .D(I[8]), .Q(SB_DFF_inst8_Q));
SB_DFF SB_DFF_inst9 (.C(CLK), .D(I[9]), .Q(SB_DFF_inst9_Q));
SB_DFF SB_DFF_inst10 (.C(CLK), .D(I[10]), .Q(SB_DFF_inst10_Q));
SB_DFF SB_DFF_inst11 (.C(CLK), .D(I[11]), .Q(SB_DFF_inst11_Q));
SB_DFF SB_DFF_inst12 (.C(CLK), .D(I[12]), .Q(SB_DFF_inst12_Q));
SB_DFF SB_DFF_inst13 (.C(CLK), .D(I[13]), .Q(SB_DFF_inst13_Q));
SB_DFF SB_DFF_inst14 (.C(CLK), .D(I[14]), .Q(SB_DFF_inst14_Q));
SB_DFF SB_DFF_inst15 (.C(CLK), .D(I[15]), .Q(SB_DFF_inst15_Q));
SB_DFF SB_DFF_inst16 (.C(CLK), .D(I[16]), .Q(SB_DFF_inst16_Q));
SB_DFF SB_DFF_inst17 (.C(CLK), .D(I[17]), .Q(SB_DFF_inst17_Q));
SB_DFF SB_DFF_inst18 (.C(CLK), .D(I[18]), .Q(SB_DFF_inst18_Q));
SB_DFF SB_DFF_inst19 (.C(CLK), .D(I[19]), .Q(SB_DFF_inst19_Q));
SB_DFF SB_DFF_inst20 (.C(CLK), .D(I[20]), .Q(SB_DFF_inst20_Q));
SB_DFF SB_DFF_inst21 (.C(CLK), .D(I[21]), .Q(SB_DFF_inst21_Q));
assign O = {SB_DFF_inst21_Q,SB_DFF_inst20_Q,SB_DFF_inst19_Q,SB_DFF_inst18_Q,SB_DFF_inst17_Q,SB_DFF_inst16_Q,SB_DFF_inst15_Q,SB_DFF_inst14_Q,SB_DFF_inst13_Q,SB_DFF_inst12_Q,SB_DFF_inst11_Q,SB_DFF_inst10_Q,SB_DFF_inst9_Q,SB_DFF_inst8_Q,SB_DFF_inst7_Q,SB_DFF_inst6_Q,SB_DFF_inst5_Q,SB_DFF_inst4_Q,SB_DFF_inst3_Q,SB_DFF_inst2_Q,SB_DFF_inst1_Q,SB_DFF_inst0_Q};
endmodule

module Counter22_COUT (output [21:0] O, output  COUT, input  CLK);
wire [21:0] Add22_COUT_inst0_O;
wire  Add22_COUT_inst0_COUT;
wire [21:0] Register22_inst0_O;
Add22_COUT Add22_COUT_inst0 (.I0(Register22_inst0_O), .I1({1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b1}), .O(Add22_COUT_inst0_O), .COUT(Add22_COUT_inst0_COUT));
Register22 Register22_inst0 (.I(Add22_COUT_inst0_O), .O(Register22_inst0_O), .CLK(CLK));
assign O = Register22_inst0_O;
assign COUT = Add22_COUT_inst0_COUT;
endmodule

module main (output  D5, input  CLKIN);
wire [21:0] Counter22_COUT_inst0_O;
wire  Counter22_COUT_inst0_COUT;
Counter22_COUT Counter22_COUT_inst0 (.O(Counter22_COUT_inst0_O), .COUT(Counter22_COUT_inst0_COUT), .CLK(CLKIN));
assign D5 = Counter22_COUT_inst0_O[21];
endmodule

Notice that the top-level module contains two arguments (ports), D5 and CLKIN. D5 has been configured as an output, and CLKIN as an input.

The mapping from these named arguments to pins is contained in the PCF (physical constraint file).


In [7]:
%cat build/blink.pcf


set_io D5 95
set_io CLKIN 21

D5 is connected to pin 95 and CLKIN is connected to pin 21.